#!/bin/sh
#description: get static linux binaries from s.minos.io and mirrors
#usage: static-get package

#example: static-get git
#git-1.8.2.1-3.tar.gz:sha512sum

VERSION="2017.07.04-12:19"

_usage() {
    printf "%b\\n" "Usage: ${progname} [OPTION]... PACKAGE ..."
    printf "%b\\n" "Retrieve static linux binaries from s.minos.io and mirrors."
    printf "\\n"
    printf "%b\\n" "  -d, --download [dir]        write in the specified directory"
    printf "%b\\n" "  -o, --output [file]         write to file"
    printf "%b\\n" "  -s, --search [pattern]      search packages by pattern"
    printf "%b\\n" "  -x, --extract               extract after download"
    printf "%b\\n" "  -n, --dry-run               perform a trial run with no changes made"
    printf "%b\\n" "  -c, --clean-cache           remove temporal files"
    printf "\\n"
    printf "%b\\n" "  -m, --mirror [s.minos.io]   set mirror"
    printf "%b\\n" "  -a, --arch   [x86_64|i686]  set architecture"
    printf "%b\\n" "  -f, --format [gz|bz2|xz]    set compress format"
    printf "%b\\n" "  -k, --checksum [sha512|md5] set checksum"
    printf "%b\\n" "  -t, --distro [all|bifrost|morpheus|rlsd2|misc] set distro"
    printf "\\n"
    printf "%b\\n" "  -U, --update                update this program to latest version"
    printf "%s\\n" "  -V, --version               display version"
    printf "%b\\n" "  -v, --verbose               turn on verbose mode"
    printf "%b\\n" "  -q, --quiet                 suppress non-error messages"
    printf "%b\\n" "  -h, --help                  show this message and exit"
}

_basename() {
    [ -z "${1}" ] && return 1 || _basename__name="${1}"
    [ -z "${2}" ] || _basename__suffix="${2}"
    case "${_basename__name}" in
        /*|*/*) _basename__name="${_basename__name##*/}"
    esac

    if [ -n "${_basename__suffix}" ] && [ "${#_basename__name}" -gt "${#2}" ]; then
        _basename__name="${_basename__name%$_basename__suffix}"
    fi

    printf "%s\\n" "${_basename__name}"
}

_die() {
    [ -z "${1}" ] || printf "%s\\n" "${*}" >&2
    _usage; >&2; exit 1
}

_update() {
    _u__current_file="$(cd "$(dirname "${0}")" && pwd)/${progname}"
    _u__current_version_long="$(awk -F\" \
        '/VERSION=/{print $2;exit}' "${_u__current_file}" 2>/dev/null)"
    _u__current_version="$(printf "%s\\n" "${_u__current_version_long}" | \
        awk '{gsub(/[\.:]/,"");gsub(/-/,"");print;exit}' 2>/dev/null)"
    [ -z "${_u__current_version}" ] && printf "%s\\n%s\\n%s\\n"           \
        "ERROR: Failed to detect current version, please update manually" \
        "${progname}         = ${_u__current_file}"                       \
        "${progname} version = ${_u__current_version}" >&2 && return 1

    command -v "wget" >/dev/null 2>&1 || command -v "curl" >/dev/null 2>&1 || \
        { printf "%s\\n" "ERROR: Install either 'wget' or 'curl' to upgrade" >&2; return 1; }

    _u__url="https://raw.githubusercontent.com/minos-org/minos-static/master/${progname}"
    _u__tmpfile="/tmp/${progname}.${$}.update"

    _u__error_msg="$(wget -q -O- --no-check-certificate "${_u__url}" \
        > "${_u__tmpfile}" 2>&1   || curl -s "${_u__url}"  \
        > "${_u__tmpfile}" 2>&1)" || { printf "%s\\n%s\\n" \
        "ERROR: Failed to fetch update, please try later or update manually" \
        "${_u__error_msg}" >&2; return 1; }

    _u__update_version_long="$(awk -F\" \
        '/VERSION=/{print $2;exit}' "${_u__tmpfile}" 2>/dev/null)"
    _u__update_version="$(printf "%s\\n" "${_u__update_version_long}" | awk \
        '{gsub(/[\.:]/,"");gsub(/-/,"");print;exit}' 2>/dev/null)"
    [ -n "${_u__update_version}" ] || _u__update_version="0"

    if [ "${_u__current_version}" -lt "${_u__update_version}" ]; then
        printf "%s %s\\n" "Updating from version" \
            "${_u__current_version_long} to ${_u__update_version_long} ..."
        chmod +x "${_u__tmpfile}"
        if ! mv -f "${_u__tmpfile}" "${_u__current_file}" 2>/dev/null; then
            printf "%s\\n" "ERROR: no write permissions on ${_u__current_file}" >&2
            printf "%s\\n" "INFO : trying with sudo ..." >&2
            if command -v "sudo" >/dev/null 2>&1; then
                sudo mv "${_u__tmpfile}" "${_u__current_file}" || return 1
            else
                printf "%s\\n" "ERROR: sudo isn't available, exiting ..." >&2
                rm -rf "${_u__tmpfile}"
                return 1
            fi
        fi
        printf "%s %s\\n" "${progname} is up-to-date (${_u__update_version_long})"
        return 0
    fi
    printf "%s %s\\n" "${progname} is up-to-date (${_u__current_version_long})"
    rm -rf "${_u__tmpfile}"
}

_show_version() {
    printf "%s\\n" "${progname} ${VERSION}"
}

_mkdir_p() { #portable mkdir -p
    [ -n "${1}" ] || return 1
    for _mkdir_p__dir in "${@}"; do
        [ -d "${_mkdir_p__dir}" ] && continue
        _mkdir_p__IFS="${IFS}"
        IFS="/"
        set -- ${_mkdir_p__dir}
        IFS="${_mkdir_p__IFS}"
        (
        case "${_mkdir_p__dir}" in
            /*) cd /; shift ;;
        esac
        for _mkdir_p__subdir in "${@}"; do
            [ -z "${_mkdir_p__subdir}" ] && continue
            if [ -d "${_mkdir_p__subdir}" ] || mkdir "${_mkdir_p__subdir}"; then
                if cd "${_mkdir_p__subdir}"; then
                    :
                else
                    printf "%s\\n" "_mkdir_p: Can't enter ${_mkdir_p__subdir} while creating ${_mkdir_p__dir}"
                    return 1
                fi
            else
                return 1
            fi
        done
        )
    done
}

_verbose() {
    [ -z "${1}" ] && return 1
    [ -n "${verbose}" ] && printf "%s\\n" "${*}" || :
}

_cat() {
    [ -z "${1}" ] && return 1
    [ -z "${global_output}" ]      && output_fname="$(_basename "${1}")" || {
        [ -z "${output_counter}" ] && output_counter="0"
    }

    if [ -z "${global_output}" ]; then
        cat "${1}" >  "${directory}/${output_fname}"
    else
        case "${output_fname}" in
              -) cat "${1}"; return 0 ;;
        *\{#\}*) output_counter="$(($output_counter + 1))"
                 cat "${1}" >> "${directory}/$(printf "%s\\n" "${output_fname}" | \
                     sed "s:{#}:${output_counter}:")"
                 ;;
              *) cat "${1}" >> "${directory}/${output_fname}" ;;
        esac
    fi

    printf "%s\\n" "${directory}/${output_fname}" | sed 's:\./::' | sed "s:{#}:${output_counter}:"
}

_uniq_id_mirror() {
    printf "%s\\n" "${1%/}" | sed 's|/|##|g'
}

_fetch() {
    case "${#}" in
        1) $retriever_bin "${1}" ;;
        2)
           case "${retriever_bin}" in
               wget*) $retriever_bin -O "${2}" "${1}" >/dev/null || \
                      $retriever_bin -O "${2}" "http://${1}" >/dev/null ;;
               curl*) ${retriever_bin# -O} -o "${2}" "${1}"  >/dev/null ;;
               *) return 1 ;;
           esac
           ;;
        *) return 1 ;;
    esac
}

_get_available_distros() {
    #$1 => mirror, eg: s.minos.io|s.minos.io/archive
    [ -z "${1}" ] && return 1
    [ -z "${uniq_id_mirror}" ] && uniq_id_mirror="$(_uniq_id_mirror "${1}")"
    _mkdir_p "${tmpcache}.${uniq_id_mirror}"
    if [ ! -f "${tmpcache}.${uniq_id_mirror}/index.html" ]; then
        cd "${tmpcache}.${uniq_id_mirror}"
        _fetch "${1}" index.html
    fi
    awk  -F'"' '/href/ {if ($2 != "../" && $2 != "robots.txt") {sub(/\//,"",$2);print $2}}' \
        "${tmpcache}.${uniq_id_mirror}/index.html" 2>/dev/null
}

_get_available_archs() {
    #$1 => mirror, eg: s.minos.io|s.minos.io/archive
    #$2 => distro, eg: bifrost
    [ -z "${2}" ] && return 1
    [ -z "${uniq_id_mirror}" ] && uniq_id_mirror="$(_uniq_id_mirror "${1}")"

    _mkdir_p "${tmpcache}.${uniq_id_mirror}/${2}"

    if [ ! -f "${tmpcache}.${uniq_id_mirror}/${2}/index.html" ]; then
        cd "${tmpcache}.${uniq_id_mirror}/${2}"
        _fetch "${1}/${2}" index.html
    fi
    awk  -F'"' '/href/ {if ($2 != "../") {sub(/\//,"",$2);print $2}}' \
        "${tmpcache}.${uniq_id_mirror}/${2}/index.html" 2>/dev/null
}

_get_package_indexes() {
    #$1 => mirror, eg: s.minos.io|s.minos.io/archive
    [ -z "${1}" ] && return 1
    [ -z "${available_distros}" ] && available_distros="$(_get_available_distros "${1}")"
    [ -z "${uniq_id_mirror}" ]    && uniq_id_mirror="$(_uniq_id_mirror "${1}")"
    for gdistro in ${available_distros}; do
        available_arch="$(_get_available_archs "${1}" "${gdistro}")"
        for garch in ${available_arch}; do
            _mkdir_p "${tmpcache}.${uniq_id_mirror}/${gdistro}/${garch}"
            if [ ! -f "${tmpcache}.${uniq_id_mirror}/${gdistro}/${garch}/${checksum}sum.txt" ]; then
                (
                cd "${tmpcache}.${uniq_id_mirror}/${gdistro}/${garch}"
                if _fetch "${1}/${gdistro}/${garch}/${checksum}sum.tar.${compress_format}"; then
                    ${compress_bin} < "${checksum}sum.tar.${compress_format}" | tar xf -
                else
                    head -3 "${checksum}sum.tar.${compress_format}" >&2
                fi
                )
            fi
        done
    done
}

_set_defaults() {
    [ -z "${mirror}" ]         && mirror="s.minos.io/archive"
    [ -z "${uniq_id_mirror}" ] && uniq_id_mirror="$(_uniq_id_mirror "${mirror}")"
    [ -z "${distro}" ]         && distro="all"
    [ -z "${arch}" ]           && arch="$(uname -m)"
    [ -z "${directory}" ]      && directory="."
    [ X"${output}" = X"-" ]    && directory="."

    if [ -z "${dry_run}" ]; then
        _mkdir_p "${directory}" || { printf "%s\\n" "unable to create ${directory}, exiting"; exit 1; }
    fi

    case "${arch}" in
        x86|386|486|586|i86|32) arch="i686" ;;
        amd64|64) arch="x86_64" ;;
        i686|x86_64) : ;;
        *) _verbose "${arch} is not a valid architecture, using system default: $(uname -m)"; arch="$(uname -m)" ;;
    esac

    case "${compress_format}" in
        gz)  compress_bin="zcat"  ;;
        bz2) compress_bin="bzcat" ;;
        xz)  compress_bin="xzcat" ;;
        *)   if command -v "xzcat" >/dev/null 2>&1; then
                 compress_format="xz"
                 compress_bin="xzcat"
             elif command -v "bzcat" >/dev/null 2>&1; then
                 compress_format="bz2"
                 compress_bin="bzcat"
             elif command -v "zcat" >/dev/null 2>&1; then
                 compress_format="gz"
                 compress_bin="zcat"
             else
                printf "%s\\n" "install either 'gzip', 'bzip2' or 'xz' to run this program" >&2
                exit 1
             fi
             ;;
    esac

    case "${checksum}" in
        sha512) checksum_bin="shasum -a 512" ;;
        md5)    checksum_bin="md5sum" ;;
        *)      if command -v "shasum" >/dev/null 2>&1; then
                    checksum="sha512"
                    checksum_bin="shasum -a 512"
                elif command -v "md5sum" >/dev/null 2>&1; then
                    checksum="md5"
                    checksum_bin="md5sum"
                else
                    printf "%s\\n" "install either 'md5sum' or 'shasum' to run this program" >&2
                    exit 1
                fi
                ;;
    esac

    if command -v "wget" >/dev/null 2>&1; then
        retriever_bin="wget -q"
    elif command -v "curl" >/dev/null 2>&1; then
        retriever_bin="curl -s -O"
    elif command -v "fetch" >/dev/null 2>&1; then
        retriever_bin="fetch"
    else
        printf "%s\\n" "install either 'wget', 'curl' or 'fetch' to run this program" >&2
        exit 1
    fi

    available_distros="$(_get_available_distros "${mirror}")"
    if [ -z "${available_distros}" ]; then
        printf "%s\\n" "no available packages at ${mirror}, it may be an incorrect url or could be down temporarily" >&2
        exit 1
    fi
}

_static_search() {
    #$1 => search pattern
    [ -z "${1}" ] && return 1

    _set_defaults
    _get_package_indexes "${mirror}"

    _ssearch__sanitazed_pattern="$(printf "%s\\n" "${1}" | \
    tr -cd 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890.-')"
    [ X"${distro}" = X"all" ] && distro=""

    _ssearch__result="$(find "${tmpcache}.${uniq_id_mirror}"/ -name "${checksum}sum.txt"   | \
        grep -- "${distro}" | xargs grep -- "${_ssearch__sanitazed_pattern}" | grep -- "${arch}" | \
        grep -- "tar.${compress_format}" | sed "s:${tmpcache}.::g;s:##:/:g;s:/${checksum}sum.txt\:: :g")"

    if [ -n "${_ssearch__result}" ]; then
        if [ "${verbose}" ]; then
            printf "%s\\n" "${_ssearch__result}" | awk '{printf "%s %s %s\n", $3, $2, $1}' | \
                grep -- "${1}" | awk '{printf "%s:%s:%s\n", $3, $1, $2}'
        else
            printf "%s\\n" "${_ssearch__result}" | awk '{printf "%s:%s\n", $3, $2}' | \
                grep -- "${1}" | cat
        fi
    else
        return 1
    fi
}

_static_get() {
    [ -z "${1}" ] && return 1 || _set_defaults
    for package in "${@}"; do
        #extra_params="-m ${mirror} -a ${arch} -f ${compress_format} -c ${checksum} -t ${distro}"
        #_sget__file="$("${0}" ${extra_params} -v -n -s "^${package}" | sort -nr |head -1)"
        _sget__file="$(verbose=1 dry_run=1 _static_search "^${package}" | sort -nr |head -1)"
        [ -z "${_sget__file}" ] && _verbose "No matches for '${package}', skipping..." && continue
        [ -z "${uniq_id_mirror}" ] && uniq_id_mirror="$(_uniq_id_mirror "${mirror}")"

        _sget__hash="${_sget__file##*:}"
        _sget__file="${_sget__file%:$_sget__hash}" #remove hash
        _sget__file="$(printf "%s\\n" "${_sget__file}" | tr ':' '/')"
        _sget__bfile="$(_basename "${_sget__file}")"

        for i in 1 2; do #download on first run, write/extract on second
            if [ "${dry_run}" ]; then
                if [ "${output_fname}" ]; then
                    case "${output_fname}" in
                        *\{#\}*) [ -z "${output_counter}" ] && output_counter="0"
                             output_counter="$(($output_counter + 1))"
                             ;;
                    esac
                    printf "%s\\n" "${directory}/${output_fname}" | sed 's:\./::' | sed "s:{#}:${output_counter}:"
                else
                    printf "%s\\n" "${directory}/${_sget__bfile}" | sed 's:\./::'
                    [ "${extract}" ] && printf "%s\\n" "${directory}/${_sget__bfile%%.tar*}/" | sed 's:\./::'
                fi
                break
            fi

            if [ -f "${tmpcache}.${uniq_id_mirror}/${_sget__bfile}" ]; then
                _sget__hash_new="$($checksum_bin "${tmpcache}.${uniq_id_mirror}/${_sget__bfile}" | awk '{print $1}')"

                if [ "${_sget__hash}" = "${_sget__hash_new}" ]; then
                    _cat "${tmpcache}.${uniq_id_mirror}/${_sget__bfile}"

                    if [ "${extract}" ]; then
                        _mkdir_p "${directory}/${_sget__bfile%%.tar*}"
                        (cd "${directory}/${_sget__bfile%%.tar*}" && \
                        ${compress_bin} < "../${_sget__bfile}"  | tar xf -) && \
                        printf "%s\\n" "${directory}/${_sget__bfile%%.tar*}/" | sed 's:\./::'
                    fi
                    break
                else
                    printf "'%s' doesn't match hashsum => '%s', clean the cache (%s) and try again\\n" \
                        "${directory}/${_sget__bfile}" "${_sget__hash}" \
                        "$(_usage | awk '/remove temporal/ {sub(/,/, ""); print $1 "|" $2}')" | sed 's:\./::'
                    rm -f "${tmpcache}.${uniq_id_mirror}/${_sget__bfile}"
                fi
            else
                (cd "${tmpcache}.${uniq_id_mirror}/" && _fetch "${_sget__file}")
            fi
        done
    done
}

progname="$(_basename "${0}")"
tmpcache="/tmp/static-get"

if [ ! -t 0 ]; then
    #there is input comming from pipe or file, add it to the end of $@
    set -- "${@}" $(cat)
fi

[ "${#}" -eq "0" ] && _die

for arg in "${@}"; do #parse options
    case "${arg}" in
        -h|--help)    _usage;  exit ;;
        -U|--update)  _update; exit "${?}" ;;
        -V|--version) _show_version; exit "${?}" ;;
        -q|--quiet)   quiet="1";   shift ;;
        -v|--verbose) verbose="1"; shift ;;
        -x|--extract) extract="1"; shift ;;
        -n|--dry-run) dry_run="1"; shift ;;
        '-d'|'--download'|-d*|--download*)
            case "${arg}" in
                '-d'|'--download')
                    if [ "${#}" -gt "1" ]; then
                        case "${2}" in
                            -*) _die "Option '${arg}' requires a parameter";;
                        esac
                        shift; directory="${1}"; [ "${1}" ] && shift
                    else
                        _die "Option '${arg}' requires a parameter"
                    fi
                    ;;
                -d*) directory="${1#-d}"; shift ;;
                --download*) directory="${1#--download}"; shift ;;
            esac
            ;;

        '-o'|'--output'|-o*|--output*)
            case "${arg}" in
                '-o'|'--output')
                    if [ "${#}" -gt "1" ]; then
                        case "${2}" in
                            '-') : ;; #special stdout character
                             -*) _die "Option '${arg}' requires a parameter" ;;
                        esac
                        shift; output_fname="${1}"; global_output="1"; [ "${1}" ] && shift
                    else
                        _die "Option '${arg}' requires a parameter"
                    fi
                    ;;
                -o*) output_fname="${1#-o}"; shift; global_output="1" ;;
                --output*) output_fname="${1#--output}"; shift; global_output="1" ;;
            esac
            ;;

        '-s'|'--search'|-s*|--search*)
            case "${arg}" in
                '-s'|'--search')
                    if [ "${#}" -gt "1" ]; then
                        case "${1}" in
                            -*) shift; for opt in "${@}"; do
                                    #last parameter as search regex
                                    search_pattern="${opt}"
                                done
                                ;;
                            *) shift; search_pattern="${1}"
                               [ "${1}" ] && shift ;;
                        esac
                    else
                        search_pattern="."
                    fi
                    ;;
                -s*) search_pattern="${1#-s}"; shift ;;
                --search*) search_pattern="${1#--search}"; shift ;;
            esac
            ;;

        '-a'|'--arch'|-a*|--arch*)
            case "${arg}" in
                '-a'|'--arch')
                    if [ "${#}" -gt "1" ]; then
                        case "${2}" in
                            -*) _die "Option '${arg}' requires a parameter" ;;
                        esac
                        shift; arch="${1}"; [ "${1}" ] && shift
                    else
                        _die "Option '${arg}' requires a parameter"
                    fi
                    ;;
                -a*) arch="${1#-a}"; shift ;;
                --arch*) arch="${1#--arch}"; shift ;;
            esac
            ;;

        '-m'|'--mirror'|-m*|--mirror*)
            case "${arg}" in
                '-m'|'--mirror')
                    if [ "${#}" -gt "1" ]; then
                        case "${2}" in
                            -*) _die "Option '${arg}' requires a parameter" ;;
                        esac
                        shift; mirror="${1}"; [ "${1}" ] && shift
                    else
                        _die "Option '${arg}' requires a parameter"
                    fi
                    ;;
                -m*) mirror="${1#-m}"; shift ;;
                --mirror*) mirror="${1#--mirror}"; shift ;;
            esac
            ;;

        '-k'|'--checksum'|-k*|--checksum*)
            case "${arg}" in
                '-k'|'--checksum')
                    if [ "${#}" -gt "1" ]; then
                        case "${2}" in
                            -*) _die "Option '${arg}' requires a parameter" ;;
                        esac
                        shift; checksum="${1}"; [ "${1}" ] && shift
                    else
                        _die "Option '${arg}' requires a parameter"
                    fi
                    ;;
                -k*) checksum="${1#-k}"; shift ;;
                --checksum*) checksum="${1#--checksum}"; shift ;;
            esac
            ;;

        '-t'|'--distro'|t*|--distro*)
            case "${arg}" in
                '-t'|'--distro')
                    if [ "${#}" -gt "1" ]; then
                        case "${2}" in
                            -*) _die "Option '${arg}' requires a parameter" ;;
                        esac
                        shift; distro="${1}"; [ "${1}" ] && shift
                    else
                        _die "Option '${arg}' requires a parameter"
                    fi
                    ;;
                -t*) distro="${1#-t}"; shift ;;
                --distro*) distro="${1#--distro}"; shift ;;
            esac
            ;;

        '-f'|'--format'|-f*|--format*)
            case "${arg}" in
                '-f'|'--format')
                    if [ "${#}" -gt "1" ]; then
                        case "${2}" in
                            -*) _die "Option '${arg}' requires a parameter" ;;
                        esac
                        shift; compress_format="${1}"; [ "${1}" ] && shift
                    else
                        _die "Option '${arg}' requires a parameter"
                    fi
                    ;;
                -f*) compress_format="${1#-f}"; shift ;;
                --format*) compress_format="${1#--format}"; shift ;;
            esac
            ;;

        -c|--clean-cache) rm -rf "${tmpcache}".* && \
            [ -z "${quiet}" ] && printf "%s\\n" "Cache cleared successfully"
            exit ;;
        '-') : ;; #special stdout character
        -*) _die "${progname}: unrecognized option '${arg}'" >&2 ;;
    esac
done

if [ -z "${search_pattern}" ]; then
    [ "${#}" -eq "0" ] && _die "${progname}: missing arguments"
    _static_get "${@}"
else
    _static_search "${search_pattern}"
fi
